<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<div id="container">
    {{ msg }}<br>
    <input class="inp-text" type="text" v-model="inpText"/>
    <p>{{ inpText }}</p>
    <div class="text-box">
        <p class="show-text">{{ msg }}</p>
    </div>
</div>
</body>
<script>
    /**
     * 获取所有的 vue-dom 虚拟节点
     * @param node
     * @param vm
     * @param flag
     * @returns {*|DocumentFragment}
     */
    function virtualDom(node, vm, flag){
        flag = flag || document.createDocumentFragment();
        let child;
        while(child = node.firstChild){
            // 编译第一个子节点,然后给这个子节点添加相应的
            compile(child, vm);
            // 添加进flag
            flag.appendChild(child);
            if(child.firstChild){
                virtualDom(child, vm, flag);
            }
        }
        return flag;
    }

    /**
     * 编译，将有v-model属性的节点，或者text节点，解析,然后将其变为相应的dom元素
     * @param node
     * @param vm
     */
    function compile(node, data){
        let reg = /\{\{(.*)\}\}/g;//匹配双绑的双大括号
        if(node.nodeType === 1){
            let attr = node.attributes;
            //解析节点的属性
            for(let i = 0, len = attr.length;i < len; i++){
                if(attr[i].nodeName === 'v-model'){
                    let name = attr[i].nodeValue;
                    // node.value = vm.data[name];//讲实例中的data数据赋值给节点
                    // node里面的name属性订阅了 Publisher,Publisher更新了就会通知所有的订阅者
                    let subscriber = new Subscriber(node, data, name);
                    // 将订阅者，订阅到发布者上面
                    subscriber.register(publisher)

                    // ------------------------添加监听事件
                    node.addEventListener('keyup', function(e){
                        data[name] = e.target.value;
                    }, false);
                }
            }
            if(node.hasChildNodes()){
                node.childNodes.forEach((item) => {
                    compile(item, data);
                });
            }
        }
        //如果节点类型为text
        if(node.nodeType === 3){
            if(reg.test(node.nodeValue)){
                let name = RegExp.$1;//获取匹配到的字符串
                name = name.trim();
                // node.nodeValue = vm.data[name];
                let subscriber = new Subscriber(node, data, name);
                // 将订阅者，订阅到发布者上面
                subscriber.register(publisher)
            }
        }
    }

    let publisher = null
    /**
     * 对每个对象进行重新复写，让其getter setter方法重新生成
     * */
    function defineReact(obj, key, value){
        publisher = new Publisher();
        Object.defineProperty(obj, key, {
            set: function(newValue){
                console.log(`触发setter`);
                value = newValue;
                console.log(value);
                // 通知所有的订阅者，
                publisher.notify()
            },
            get: function(){
                console.log(`触发getter`);
                if(Publisher.global){ //这里为什么来添加判断条件，主要是让publisher.add只执行一次，初始化虚拟dom编译的时候来执行
                    publisher.add(Publisher.global); // 添加订阅者
                }
                return value;
            }
        });
    }
    /**
     * 获取到每个对象
     * */
    function observe(obj, vm){
        Object.keys(obj).forEach((key) => {
            defineReact(vm, key, obj[key]);
        })
    }



    /******************************************************** 以下是订阅发布者模式 *****************************************************/
    // 一、声明订阅者，实现 new Watcher( vm=id为 mvvm 的dom节点 , node=里面的单个子节点, name=里面的单个子节点的名字 ) ，实现了将每个 节点 放到
    // 含义是  vm 里面的name 订阅了 Publisher
    function Subscriber(node,data,name){
        Publisher.global = this;
        this.name = name;
        this.node = node;
        this.data = data;

        this.update();
        Publisher.global = null;
    }
    Subscriber.prototype = {
        constructor: Subscriber,
        update: function(){
            let node = this.node;
            let name = this.name;
            switch(this.node.nodeType){
                case 1:
                    node.value = this.data[name];
                    break;
                case 3:
                    node.nodeValue = this.data[name];
                    break;
                default:
                    break;
            }
        },
        // 注册这个订阅者到发布者的属性 subscribers 上面
        register:function (publisher) {
            publisher.add(this)
        }
    };
    // 二、声明发布者，发布给每个订阅者 ,观察者模式
    function Publisher(){
        this.subscribers = [];
    }
    Publisher.prototype = {//在原型上声明“发布消息”方法
        add:function(sub){//这里定义增加订阅者的方法
            this.subscribers.push(sub);
        },
        notify:function(){//这里定义触发订阅者update()的通知方法
            this.subscribers.forEach(function(sub){
                console.log(sub);
                sub.update();//下列发布者的更新方法
            })
        }
    }

    /**
     *  到这，我们已经实现了：
     *  修改输入框内容 => 触发修改vm实例里的属性值 => 触发set&get方法
     *  订阅成功 => 发布者发出通知notify() => 触发订阅者的update()方法
     *  接下来重点要实现的是：如何去更新视图，同时把订阅——发布者模式进去watcher观察者模式？
     */

    /**
     * 声明一个vue的对象
     * @param options
     * @constructor
     */
    function Vue(options){
        this.data = options.data;
        let id = options.el;

        // 一、对每个vue 获取到的属性赋予get,set属性方法，使每个属性能够被监听到
        observe(this.data, this); // 将每个data属相绑定到Vue的实例上this
        // 二、获取到当前的ID下的dom元素，下面的所有的元素进行解析(compile函数：一、解析 {{}} 双括号，二、解析 v-model 属性)
        let container = document.getElementById(id);
        let dom = virtualDom(container,this);
        // 三、将获取到的所有的元素，append到当前的id下面
        container.appendChild(dom);

    }
    //随后使用这个vue对象
    let Demo = new Vue({
        el:'container',
        data:{
            msg:'Hel2312123loWorld',
            inpText:'123333'
        }
    })


</script>
</html>
